-- SummonM
-- Created by panyl
-- 召唤兽模块

module("SummonM", package.seeall);

-- 所有的规则处理子模块
local rules = {};

local summons = {};

local startDungeon;
local whenLeaveDungeon;
local whenSummonDie;
local reviveCallSummon = {};

-- 内部函数声明
local roundStep;

-- 载入配置表
local _init = false;
local loadCsv = function()
    if _init then return end
    _init = true;

    -- 转换下summon信息表
    local summon_CSV = get_summon_CSV();
    for i = 1, #summon_CSV do
        summons[summon_CSV[i].class_id] = summon_CSV[i];

        -- 解析动作帧范围
        parseActionFrameRange(summon_CSV[i]);

        local info = summon_CSV[i];
        local dbase = info["dbase"] or {};
        if dbase["revive_call"]  == 1 then
            -- 玩家复活召唤(花费探索点的复活)
            table.insert(reviveCallSummon, info.class_id);
        end
    end

    summon_CSV = {};
end

function init()
    if not _init then
        loadCsv();

        -- 载入所有的规则处理子模块
        rules = LOAD_PATH("game/logic/module/summon");
    end

    EventMgr.removeAll("SummonM");

    DungeonServiceM.registerStartDungeonCB("summon", startDungeon);

    -- 关注离开地牢事件
    EventMgr.register("SummonM", event.LEAVE_DUNGEON, whenLeaveDungeon);

    -- 召唤兽死亡事件
    EventMgr.register("SummonM", event.SUMMON_DIE, whenSummonDie);

    -- 关注层数回退（大地之门等技能）的事件
    EventMgr.register("SummonM", event.BACK_FLOOR, function()
        whenEnterNext();
    end);

    -- 关注玩家复活事件
    EventMgr.register("SummonM", event.COST_SP_HERO_REVIVE, whenHeroRevive)

    CombatM.registerEndRoundCB("check_summon_action", roundStep);
end

whenLeaveDungeon = function()
    ME.user.dbase:delete("summon");
    ME.user.dbase:deleteTemp("summon_list");
    ME.user.dbase:delete("summon_prop");
    ME.user.dbase:delete("summon_building");
end

startDungeon = function()
    if UnrealDungeonM.isInUnreal() then
        -- 如果是进入夹层，不处理
        return;
    end

    -- 如果没有召唤兽数据
    local summonInfo = ME.user.dbase:query("summon", {});

    -- 这里先删掉，召唤的时候会再找出来的
    ME.user.dbase:delete("summon");

    local newInfo = {};
    local poses = table.keys(summonInfo);
    local summonList = table.values(summonInfo);

    -- 排序一下，因为boss层位置较少，需要按照权重筛选一遍
    local function sortCmp(a, b)
        local AclassId = a.class_id;
        local BclassId = b.class_id;
        local Aorder = query(AclassId, "order");
        local Border = query(BclassId, "order");
        local Apos = a.pos or 0;
        local Bpos = b.pos or 0;

        return 100000 * Aorder + 1000 * AclassId + Apos > 100000 * Border + 1000 * BclassId + Bpos;
    end
    table.sort(summonList, sortCmp);

    -- 指挥官召唤兽位置固定最后一个
    local commanderId = FormulaM.invoke("GET_COMMANDER_SUMMON_ID");

    -- 这里需要排序，确保和服务器遍历顺序一致
    for index = 1, #summonList do
        local data = summonList[index];

        local centerPos = data["normal_pos"];
        if DungeonInstanceM.isInInstance() then
            -- 中心格子
            centerPos = data["instance_pos"];
            if not centerPos then
                centerPos = 13;
            end
        end

        -- 找个位置，某些召唤兽需要替换其他格子
        local newPos;
        if query(data["class_id"], "is_replace") == 1 then
            newPos = findSummonPos(centerPos, true);
        else
            newPos = findSummonPos(centerPos);
        end

        if commanderId == data["class_id"] then
            newPos = FormulaM.invoke("GET_COMMANDER_SUMMON_POS");
        end

        if newPos <= 0 then
            -- 没位置了，提示
            doAlert(string.format(getLocStr("leave_you_away"), query(data["class_id"], "name")));
            return;
        end

        data["pos"] = newPos;

        data["isDelay"] = "summon";

        doSummon(data["class_id"], data, true);
    end

    -- 还有待召唤的（比如捆绑的野猪出副本之后才召唤的）
    local toBeSummon = ME.user.dbase:query("to_be_summon", {});
    ME.user.dbase:delete("to_be_summon");

    for index = 1, #toBeSummon do
        local data = toBeSummon[index];

        data["isDelay"] = "summon";

        data["pos"] = nil;

        local ret = doSummon(data["class_id"], data);
        if type(ret) == "string" then
            doAlert(getLocStr(ret));
        end
    end

    -- 召唤指挥官冈布奥
    if FormulaM.invoke("IS_DO_SUMMON_COMMANDER") then
        local commanderId = FormulaM.invoke("GET_COMMANDER_SUMMON_ID");
        local commanderPos = FormulaM.invoke("GET_COMMANDER_SUMMON_POS");
        local ret = doSummon(commanderId, {["pos"] = commanderPos});

        if type(ret) == "string" then
            doAlert(getLocStr(ret));
        end
    end
end

function whenEnterNext()
    ME.user.dbase:deleteTemp("summon_list");


    local summonInfo = ME.user.dbase:query("summon");
    if not summonInfo then
        return;
    end

    for _, pos in pairs(table.keys(summonInfo)) do
        local grid = DungeonM.getGridByPos(pos);
        local summon = grid.summon;
        -- 如果不是召唤兽或者已经死亡了
        if not summon or summon:isDead() or
            summon.classId ~= summonInfo[pos]["class_id"] then
            summonInfo[pos] = nil;
        else
            -- 同步下
            sync_summon_data(summon);

            -- 下楼操作，技能cd之类的干掉
            summonInfo[pos]["cast_skill"] = nil;

            summonInfo[pos]["born"] = nil;

            summonInfo[pos]["action"] = nil;

            -- 状态不带下楼（绣球、大鼓）
            summonInfo[pos]["combat_status"] = nil;

            -- 旧位置去掉
            summonInfo[pos]["normal_pos"]   = nil;
            summonInfo[pos]["instance_pos"] = nil;
        end
    end
end

whenSummonDie = function(para)
    local summon = para["summon"];

    local pos = summon:getPos();

    -- 召唤兽死亡增加玩家属性
    local prop;
    local props = PropM.fetchProps(summon, "death_reward");
    for _, prop in ipairs(props) do
        PropM.trigger(summon, prop[1], prop[2]);
    end

    -- 如果有掉落
    local classId = summon.class_id;
    local bonus = query(classId, "dbase")["dead_bonus"];
    local grid = DungeonM.getGridByPos(pos);
    if bonus then
        grid.bonus = bonus;

        -- 设置需要闪光/漂浮
        if query(classId, "dbase")["need_shadow"] then
            grid.need_shadow = 1;
        end
        if query(classId, "dbase")["need_shining"] then
            grid.need_shining = 1
        end

        -- 抛出事件
        EventMgr.fire(event.MONSTER_DROP_PROPERTY, { ["pos"] = pos, });
    end

    -- 概率掉落，暂时仅允许一次。这里的bonus和上面的bonus不能同时出现
    local gridInfo = DungeonM.queryElement(grid.element, "args");
    local dropRate = gridInfo.drop_rate;
    local bonus = gridInfo.bonus;

    if type(dropRate) == "number" and type(bonus) == "table" and #bonus == 3 then
        local rand = DungeonM.getRandSeed("summon_rate_drop") % 1000;
        local itemId = bonus[2];

        if rand < dropRate and DungeonServiceM.getItemStat(itemId) < 1 then
            -- 概率掉落，且仅允许掉落一次
            grid.bonus = bonus;

            -- 抛出事件
            EventMgr.fire(event.MONSTER_DROP_PROPERTY, { ["pos"] = pos, });
        end
    end

    -- 指挥官召唤兽死亡后不能继续迷宫探索
    local dieLeave = query(classId, "dbase")["die_leave"];
    if dieLeave == 1 then
        EventMgr.fire(event.DIE_LEAVE_DUNGEON, {["pos"] = pos});
    end

    local summonInfo = ME.user.dbase:query("summon", {});
    local info = summonInfo[pos];
    if type(info) == "table" then
        if tonumber(info["class_id"]) == summon.classId then
           summonInfo[pos] = nil;
        end
    end

    -- 从玩家的 summon_list 中移除该召唤物
    local summon_list = ME.user.dbase:queryTemp("summon_list", {});
    for i, ob in ipairs(summon_list) do
        if ob == summon then
            summon_list[i] = nil;
            break;
        end
    end
    clean_array(summon_list);
    ME.user.dbase:setTemp("summon_list", summon_list);

    if summon:query("enhance_user") == 1 then
        -- 辅助召唤物死亡，需要刷新玩家的 summon 属性
        PropM.refreshEffect(ME.user, "summon");
    end

    -- 召唤兽死亡，玩家直接死亡
    local dieKillUser = query(classId, "dbase")["die_kill_user"];
    local heroDieTalk = query(classId, "dbase")["hero_die_talk"];
    if dieKillUser == 1 then
        local value = PropM.getShieldHit();
        ME.user:setHp(0 - math.max(0, value));
        ME.user:die({["no_revive"] = 1, ["hero_talk"] = heroDieTalk});
    end
end

-- 检索
function query(classId, path)
    local m = summons[classId];

    if not m or not path then
        return m;
    end

    return m[path];
end

-- 解析动作帧范围
function parseActionFrameRange(info)
    -- 待机、受创、攻击
    local fields = { "idle_frame", "damaged_frame", "attack_frame", "cast_frame"};

    for _, field in pairs(fields) do
        local range = info[field];
        if not string.startWith(range, "[") or not string.endWith(range, "]") then
            error(string.format("字段(%s)配置格式错误!", tostring(field)));
            return info;
        end

        local arr = string.explode(string.sub(range, 2, -2),",");
        if #arr ~= 2 then
            error(string.format("字段(%s)配置格式错误!", tostring(field)));
            return info;
        end

        info[field] = { tonumber(arr[1]), tonumber(arr[2]) };
    end

    return info;
end

-- 召唤兽初始属性
function initSummonData(classId)
    local info = query(classId);
    local ret = query(classId, "dbase") or {};
    local mod = rules[info.rule];

    if mod and mod.fetchInitData then
        -- 子模块获取
        local d = mod.fetchInitData(classId);
        if d then
            ret = table.add(d, ret);
        end
    end

    local hp = info["hp"] or 0;
    local mp = info["mp"] or 0;
    local max_mp = info["max_mp"] or 0;
    local max_hp = info["max_hp"] or hp;

    return table.add({
        ["class_id"] = classId,
        ["style"] = info["style"],
        ["hp"] = hp,
        ["mp"] = mp,
        ["max_mp"] = max_mp,
        ["max_hp"] = max_hp,
        ["attack"] = info["attack"] or 0,
        ["accuracy"] = info["accuracy"] or 0,
        ["dodge"] = info["dodge"] or 0,
        ["absorb_rate"] = info["absorb_rate"],
        ["action"] = CombatM.getRound(),
        ["born"] = CombatM.getRound(),
    }, ret);
end

-- 召唤兽消失
function summonDisappear(pos)
    local grid = DungeonM.getGridByPos(pos);
    local summon = grid.summon;

    if not summon then
        return false;
    end

    local summonInfo = ME.user.dbase:query("summon", {});
    local info = summonInfo[pos];
    if type(info) == "table" then
        if tonumber(info["class_id"]) == summon.classId then
           summonInfo[pos] = nil;
        end
    end

    grid:changeState(GRID_STATE_DISABLE);

    EventMgr.fire(event.SUMMON_DISAPPEAR, {["classId"] = summon.classId, ["pos"] = pos, ["totem"] = 1, });

    -- 从玩家的 summon_list 中移除该召唤物
    local summon_list = ME.user.dbase:queryTemp("summon_list", {});
    for i, ob in ipairs(summon_list) do
        if ob == summon then
            summon_list[i] = nil;
            break;
        end
    end
    clean_array(summon_list);
    ME.user.dbase:setTemp("summon_list", summon_list);

    if summon:query("enhance_user") == 1 then
        -- 辅助召唤物死亡，需要刷新玩家的 summon 属性
        PropM.refreshEffect(ME.user, "summon");
    end

    -- TODO: 最后再把summon对象干掉?
    grid.summon = nil;

    return true;
end

-- 创建召唤兽
function createSummon(dbase)
    local summon = Summon.new(dbase["class_id"], dbase);

    return summon;
end

-- 寻找可召唤的空位
-- @param centerPos  中心格子位置，如果传入了，则取离中心格子最近的位置
-- @param isReplace  在没有空格的情况下，是否替换指定元素，如血瓶、蓝瓶等
-- @param allowBlock 是否包含障碍格（默认不包含）
function findSummonPos(centerPos, isReplace, allowBlock)
    local idle = {};
    local rand = DungeonM.getRandSeed("summon number");

    -- 获取不允许召唤的格子
    local illegalPos = getIllegalPos();

    -- 先找出所有已使用的空位
    for pos = 1, GRID_SIZE do
        local grid = DungeonM.getGridByPos(pos);
        repeat
        -- 未被翻开
        if not grid:isOpened() then
            break;
        end

        -- 有奖励
        if grid.bonus then
            break;
        end

        -- 有钥匙
        if grid.has_key then
            break;
        end

        -- 不能是被拆毁的排炮或已失效的地板
        if (CommonTrapM.isCannon(grid) or grid.type == GRID_TYPE_ODD_FLOOR)
            and grid.state == GRID_STATE_DISABLE then
            break;
        end

        -- 有一些位置不能召唤
        if table.indexOf(illegalPos, pos) >= 0 then
            break;
        end

        -- 空格、失效的格子或者特定障碍格
        if grid.type == GRID_TYPE_EMPTY or
           grid.state == GRID_STATE_DISABLE or
           (allowBlock and grid.type == GRID_TYPE_BLOCK)then
            table.insert(idle, grid:getPos());
        end
        until true;
    end

    if #idle > 0 then
        -- TODO:
        DungeonLogM.addLog("目标: " .. tostring(centerPos) .. " 空闲的位置: " .. tableToString(idle));
        if centerPos then
            -- 指定位置，取最近的一个位置
            return findRecentDungeonPos(centerPos, idle);
        else
            -- 随机
            return idle[rand % #idle + 1];
        end
    end

    -- 没有使用的空格，则从未翻开的格子中寻找
    local replaceIdle = {};
    for pos = 1, GRID_SIZE do
        -- 有一些位置不能召唤
        if table.indexOf(illegalPos, pos) >= 0 then
            break;
        end

        local grid = DungeonM.getGridByPos(pos);
        if not grid:isOpened() then
            if grid.type == GRID_TYPE_EMPTY then
                -- 空格或者障碍
                table.insert(idle, grid:getPos());
            end

            -- 如果需要替换
            if isReplace == true then
                local replaceElements = FormulaM.invoke("GET_SUMMON_REPLACE_ELEMENTS");
                if table.indexOf(replaceElements, grid.element) >= 0 then
                    table.insert(replaceIdle, grid:getPos())
                end
            end
        end
    end

    if #idle > 0 then
        -- TODO:
        DungeonLogM.addLog("目标: " .. tostring(centerPos) .. " 空格: " .. tableToString(idle));
        if centerPos then
            -- 指定位置，取最近的一个位置
            return findRecentDungeonPos(centerPos, idle);
        else
            return idle[rand % #idle + 1];
        end
    end

    if #replaceIdle > 0 then
        -- 如果有要替换掉的格子
        DungeonLogM.addLog("目标: " .. tostring(centerPos) .. " 可替换的格子: " .. tableToString(replaceIdle));
        if centerPos then
            -- 指定位置，取最近的一个位置
            return findRecentDungeonPos(centerPos, replaceIdle);
        else
            return replaceIdle[rand % #replaceIdle + 1];
        end
    end

    return -1;
end

-- 获取不允许召唤的格子
function getIllegalPos()
    local illegalPos = {};
    if BossTentM.isHaveBossTent() then
        -- 如果在BOSS层且存在BOSS层的流浪者帐篷时，有些位置不能召唤
        illegalPos = BossTentM.getCanRefreshPos();
    end
    return illegalPos;
end

-- 根据主动怪标识查找现有的召唤兽
function findSummonsByActive(active)
    active = active or 1;   -- 默认是找主动怪

    local ret = {};
    local summon_list = ME.user.dbase:queryTemp("summon_list", {});
    for _, summon in ipairs(summon_list) do
        if summon and not summon:isDead() and
            summon:query("active") == active then
            local grid = DungeonM.getGridByPos(summon.gridPos);
            if grid then
                table.insert(ret, grid);
            else
                assert(false, "findSummonsByActive 取不到召唤物对应的格子。");
            end
        end
    end

    return ret;
end

-- 查找现有的召唤兽
function findSummons()
    local ret = {};
    local summon_list = ME.user.dbase:queryTemp("summon_list", {});
    for _, summon in ipairs(summon_list) do
        if summon and not summon:isDead() then
            local grid = DungeonM.getGridByPos(summon.gridPos);
            if grid then
                table.insert(ret, grid);
            else
                assert(false, "findSummons 取不到召唤物对应的格子。");
            end
        end
    end
    return ret;
end

-- 根据class_id查找
function findSummonsByClassId(classId)
    local ret = {};
    local excludeId = query(classId, "exclude");

    if type(excludeId) ~= "number" then
        excludeId = classId;
    end

    local summon_list = ME.user.dbase:queryTemp("summon_list", {});
    for _, summon in ipairs(summon_list) do
        if summon and (summon.classId == classId or summon.classId == excludeId) then
            local grid = DungeonM.getGridByPos(summon.gridPos);
            if grid then
                table.insert(ret, grid);
            else
                assert(false, "findSummonsByClassId 取不到召唤物对应的格子。");
            end
        end
    end

    return ret;
end

-- 能否召唤
function canDoSummon(classId)
    local info = query(classId);

    -- 可以重复的
    if info.multi == 1 then
        return true;
    end

    -- 如果是可以携带下楼的
    if info["carry_pass"] == 1 then
        local summonInfo = ME.user.dbase:query("summon", {});
        local total = #table.keys(summonInfo);

        -- 如果有魔藤
        if ME.user.bine then
            total = total + 1;
        end

        -- 如果有幽灵船
        if ME.user.ship then
            total = total + 1;
        end

        if total > 1 then
            trace("SummonM", "当前已经存在 %d 头召唤物。", total);
            return "summon_be_enough";
        end
    end

    if info["only_one"] == 1 then
        local aliveSummons = findSummonsByClassId(classId);
        for _, grid in pairs(aliveSummons) do
            if not grid.summon:isDead() then
                trace("SummonM", "还有存活的召唤兽(%s)。", info["name"]);
                return grid.summon.classId;
            end
        end
    end

    return true;
end

-- 召唤
function doSummon(classId, para, noCheck)
    local ret = canDoSummon(classId);
    if not noCheck and ret ~= true then
        return ret;
    end

    para = para or {};

    if para["hp"] and not para["max_hp"] then
        para["max_hp"] = para["hp"];
    end

    local info = query(classId);
    local mod = rules[info["rule"]];

    if mod and mod.doSummon then
        return mod.doSummon(classId, para);
    end

    apply(classId, para);
end

-- 召唤
function apply(classId, para)
    para = para or {};

    local pos = para["pos"];

    -- 指挥官召唤兽位置固定最后一个
    local commanderId = FormulaM.invoke("GET_COMMANDER_SUMMON_ID");

    if not pos then
        -- 如果是副本中，需要靠近中心格子
        if DungeonInstanceM.isInInstance() then
            pos = findSummonPos(13);
        elseif classId == commanderId then
            pos = FormulaM.invoke("GET_COMMANDER_SUMMON_POS");
        else
            pos = findSummonPos();
        end
    end

    if not pos or pos < 0 then
        trace("SummonM", "找不到有效的召唤位置。");
        return false;
    end

    DungeonLogM.addLog(string.format("[SummonM]%s出现在位置%d。", query(classId, "name"), pos));

    local class = DungeonM.getClassByType(GRID_TYPE_SUMMON);
    local dbase = initSummonData(classId);

    dbase["layer"] = DungeonM.currentLayer();
    dbase["pos"] = pos;
    dbase["active_round"] = CombatM.getRound();

    local info = table.add(dbase, para);

    -- 转换格子
    local grid = DungeonM.transformGrid(class, pos, GRID_STATE_COVER, info);

    if not info.element then
        grid.element = 100000;  -- TODO: 召唤兽的element，暂时这样处理
    end

    -- 静默翻开格子，并且指定延迟播放
    -- 如果抛出开格子事件，界面上会把怪物的背景框和血条给显示出来，而此时召唤兽的模型可能还没创建
    -- grid:open(true, true);
    grid:changeState(GRID_STATE_OPEN);

    if query(classId, "carry_pass") == 1 then
        local summonInfo = ME.user.dbase:query("summon", {});
        summonInfo[pos] = table.add({
            ["class_id"]    = classId,
            ["pos"]         = pos,
            ["normal_pos"]   = para["normal_pos"],
            ["instance_pos"] = para["instance_pos"],
            ["hp"]           = para["hp"] or grid.summon:query("hp"),
            ["mp"]           = para["mp"] or grid.summon:query("mp"),
            ["attack"]       = para["attack"] or grid.summon:query("attack"),
            ["action"]       = para["action"] or grid.summon.action,
            ["max_hp"]       = para["max_hp"] or grid.summon:query("max_hp");
        }, para);
        ME.user.dbase:set("summon", summonInfo);
    end

    -- 显示钥匙怪(在召唤的时候触发)
    local props = PropM.fetchProps(grid.summon, "show_key");
    for _, prop in ipairs(props) do
        PropM.trigger(grid.summon, prop[1], prop[2]);
    end

    -- 血量强制为1
    if grid.summon:queryAttrib("hp") <= 0 then
        grid.summon:addAttrib("hp", 1 - grid.summon:queryAttrib("hp"));
    end
    -- 魔法值强制为0
    if grid.summon:queryAttrib("mp") < 0 then
        grid.summon:addAttrib("mp", -grid.summon:queryAttrib("mp"));
    end

    -- 抛出事件
    EventMgr.fire(event.CALL_SUMMON, { ["classId"] = classId, ["pos"] = pos, ["summon"] = grid.summon });

    return true;
end

-- 寻找攻击目标
function findAttackTargets()
    local ret = {};
    for pos = 1, GRID_SIZE do
        local grid = DungeonM.getGridByPos(pos);
        local monster = grid.monster;
        if grid.state ~= GRID_STATE_DISABLE and
            grid:isOpened() and monster and not monster:isDead() then
            table.insert(ret, grid:getPos());
        end
    end

    return ret;
end

-- 统计已经存在的召唤兽
function calcExistedSummon(classId)
    if not DungeonM.isInDungeon() then
        return 0;
    end

    return #findSummonsByClassId(classId);
end

-- 是否图腾
function isTotem(grid)
    if grid.summon and grid.summon:query("totem") == 1 then
        return true;
    else
        return false;
    end
end

-- 查找可主动攻击的怪物
function getAttackSummon()
    local ret = {};
    local round = CombatM.getRound();

    local summon_list = ME.user.dbase:queryTemp("summon_list", {});
    for _, summon in ipairs(summon_list) do
        repeat
            if not summon or summon:isDead() or
                summon:query("active") ~= 1 then
                break;
            end

            local action = summon.action or 0;

            -- 可以行动了
            if  action + calcPhysicCd(summon) <= round then
                local grid = DungeonM.getGridByPos(summon.gridPos);
                if grid then
                    table.insert(ret, grid);
                else
                    assert(false, "getAttackSummon 取不到召唤物对应的格子。");
                end
            end
        until true;
    end

    return ret;
end

-- 尝试消失
function tryDisappear()
    local arr = findSummonsByActive(0);
    local round = CombatM.getRound();

    for index = 1, #arr do
        local grid = arr[index];
        local summon = grid.summon;
        local activeRound = summon.active_round or 0;
        if type(summon:query("disappear")) == "number" and
            summon:query("disappear") + activeRound < round then

            grid:changeState(GRID_STATE_DISABLE);
            local hp = summon.dbase:query("hp", 0) - summon:getHp();
            summon:setHp(hp);
            summon:die();

            -- 抛出一个事件
            EventMgr.fire(event.SUMMON_DISAPPEAR, {["classId"] = summon.classId, ["pos"] = summon:getPos(), ["totem"] = summon:query("totem"), });
        end
    end
    return true, true;
end

-- 计算召唤兽物理攻击cd
function calcPhysicCd(summon)
    local cycle = summon:query("cycle") or 1;

    -- 减回合的属性
    local prop = PropM.combine(summon, "summon_physic_cd", summon.classId);
    return PropM.apply(prop, cycle);
end

-- 召唤兽攻击
function summonAttack(summon, targetPos)
    local grid = DungeonM.getGridByPos(targetPos);

    trace("SummonM", "召唤兽(%s/%d)攻击目标%d。",
        summon:getName(), summon:getPos(), targetPos);

    -- 物理攻击
    if SkillM.physic(summon, grid.monster, true) then

        -- 抛出事件
        EventMgr.fire(event.SUMMON_ATTACK, { ["summon"] = summon,});
    end
    return true, true;
end

-- 每回合
roundStep = function(para)
    if type(para) == "table" and para.not_move then
        return false;
    end

    local round = CombatM.getRound();

    -- 消失
    tryDisappear();

    local arr = getAttackSummon();
    if #arr <= 0 then
        return false;
    end

    if ME.user:isDead() then
        trace("SummonM", "玩家已经挂了。");
        return false;
    end

    -- 所有召唤兽都尝试去攻击一次
    for index = 1, #arr do
        local grid = arr[index];
        local summon = grid.summon;

        repeat
            if summon.active_round and summon.active_round >= round then
                break;
            end

            -- 如果已经死亡了
            if summon:isDead() then
                break;
            end

            -- 可能这个时候玩家挂了
            if ME.user:isDead() then
                break;
            end

            local activeType = query(summon.classId, "active_type");
            if activeType == "physic" then
                -- 物理攻击
                triggerPhysic(summon);
            elseif activeType == "magic" then
                -- 释放魔法
                triggerMagicProp(summon);
            elseif activeType == "magic_physic" then
                -- 优先级：施法->物理攻击（如果施法就不物理攻击）
                if not triggerMagicProp(summon) then
                    triggerPhysic(summon);
                end
            elseif activeType == "magic_physic2" then
                -- 优先级：施法->物理攻击（施法后也再物理攻击）
                triggerMagicProp(summon);
                triggerPhysic(summon);
            end
        until true;
    end

    return true;
end

-- 获取所有的主动技能
function getAllSkills(summon)
    -- 属性
    local props = PropM.fetchProps(summon, "summon_skill");
    local ret = {};

    for _, prop in pairs(props) do
        table.insert(ret, prop[2]);
    end

    return ret;
end

-- 计算倒计时回合
function calcSkillCd(summon, skillId)
    local skillCd = summon.dbase:query("cast_skill", {});

    -- 用基类技能
    skillId = SkillM.findBaseSkill(skillId);

    local prop = PropM.combine(summon, "summon_skill", skillId);
    if prop[3] <= 0 then
        -- 找一遍所有的
        local props = PropM.fetchProps(summon, "summon_skill");
        for _, p in pairs(props) do
            if SkillM.findBaseSkill(tonumber(p[2])) == skillId then
                prop = p;
                break;
            end
        end

        if prop[3] <= 0 then
            return;
        end
    end

    -- cd
    local cd = prop[3];
    prop = PropM.combine(summon, "summon_skill_cd", skillId);

    cd = PropM.apply(prop, cd);

    local nextRound = (skillCd[skillId] or tonumber(summon.born)) + cd;

    return nextRound - CombatM.getRound();
end

-- 能否实战某个主动技能
function canCastSkill(summon, skillId)
    local cd = calcSkillCd(summon, skillId);
    if not cd then
        return false;
    end

    if cd > 0 then
        trace("SummonM", "技能%d冷却时间未到。\n", skillId);
        return false;
    end

    -- 消耗是否足够
    if not isMpEnough(summon, skillId) then
        trace("SummonM", "魔法值不足\n");
        return false;
    end

    return true;
end

-- 魔法值是否足够
function isMpEnough(summon, skillId)
    -- 消耗
    local dbase = SkillM.query(skillId, "dbase") or {};
    local costMp = tonumber(dbase["cost_mp"]);
    if summon:queryAttrib("mp") < costMp then
        return false;
    end

    return true;
end

-- 计算主动技消耗MP值
function calcSkillRequireMp(summon, skillId)
    local dbase = SkillM.query(skillId, "dbase") or {};
    local costMp = tonumber(dbase["cost_mp"]);
    return costMp;
end

-- 提供给UI层调用
function useSkill(pos, target_pos, skill_id)
    return DungeonActionM.go("cast_summon_skill", target_pos * 256 + pos, skill_id);
end

-- 施展主动技能
function castSkill(pos, target_pos, skill_id)
    local grid = DungeonM.getGridByPos(pos);
    local summon = grid.summon;

    -- 不是召唤兽
    if not summon then
        return false;
    end

    -- 判断能否实战
    if not canCastSkill(summon, skill_id) then
        return false;
    end

    local target = ME.user;
    if target_pos > 0 then
        grid = DungeonM.getGridByPos(target_pos);
        target = grid.monster;

        if not target then
            return false;
        end
    end

    -- 先消耗
    local dbase = SkillM.query(skill_id, "dbase") or {};
    local cost_mp = tonumber(dbase["cost_mp"]);
    if cost_mp > 0 then
        summon:costAttrib("mp", cost_mp);
    end

    -- 先记录
    local skill_cd = summon.dbase:query("cast_skill", {});
    skill_cd[SkillM.findBaseSkill(skill_id)] = CombatM.getRound();
    summon.dbase:set("cast_skill", skill_cd);

    -- 技能
    local targets = SkillM.apply(summon, target, skill_id, CombatM.getRound());

    sync_summon_data(summon);

    -- 抛出事件
    EventMgr.fire(event.SUMMON_CAST_SKILL, {["pos"] = pos, ["skillId"] = skill_id, ["targetPos"] = target_pos, ["targets"] = targets});

    EventMgr.fire(event.COMBAT_ROUND, { ["pos"] = pos, } );

    return true;
end

-- 物理攻击
function triggerPhysic(summon)
    local actionRound = summon.action or 0;
    local round = CombatM.getRound();

    -- if actionRound + calcPhysicCd(summon) > round then
    if (round - actionRound) % calcPhysicCd(summon) ~= 0 then
        trace("SummonM", "actionRound = %d, round = %d", actionRound, round);
        return false;
    end

    summon.action = round;

    -- 目标
    local targets = findAttackTargets();
    if #targets <= 0 then
        return;
    end

    local rand = DungeonM.getRandSeed("summon_attack") % #targets;
    local pos = targets[rand + 1];

    -- 召唤兽攻击
    summonAttack(summon, pos);
end

-- 触发属性
function triggerMagicProp(summon)
    -- 所有需要每回合尝试去触发的属性
    local triggerProps = PropM.getTriggerProps("round");

    local fetchProps = PropM.fetchProps;
    local trigger = PropM.trigger;

    local ret = false;
    -- 触发召唤兽属性
    for _, propId in ipairs(triggerProps) do
        local props = fetchProps(summon, propId);
        for _, prop in ipairs(props) do
            -- 触发属性
            if trigger(summon, prop[1], prop[2], ME.user) then
                -- 至少有一个属性触发了
                ret = true;
            end
        end
    end

    return ret;
end

-- 获取召唤兽的描述
function queryDesc(classId)
    local desc = query(classId, "desc");
    local prefix = "formula_"
    if type(desc) == "string" and string.startWith(desc, prefix) then
        local formulaName = string.sub(desc, string.len(prefix) + 1);
        return FormulaM.invoke(formulaName, classId);
    end

    return desc;
end

-- 获取装备属性
function getEquipmentProps(summon)
    -- 如果是海怪船长
    if MonsterCaptainM.isMonsterCaptain(summon) then
        return MonsterCaptainM.getEquipmentProps(summon);
    end

    -- 其他的暂时没有装备
    return {};
end

function sync_summon_data(summon)
    local summonInfo = ME.user.dbase:query("summon");
    local pos = summon:getPos();

    if summonInfo and summonInfo[pos] and
       summonInfo[pos]["class_id"] == summon.classId and
       query(summon.classId, "carry_pass") == 1 then
        summonInfo[pos]["hp"]     = summon:query("hp");
        summonInfo[pos]["mp"]     = summon:query("mp");
        summonInfo[pos]["attack"] = summon:query("attack");
        summonInfo[pos]["born"]   = summon:query("born");
        summonInfo[pos]["cast_skill"] = summon:query("cast_skill");
        summonInfo[pos]["action"] = summon.action;
        summonInfo[pos]["max_hp"] = summon:query("max_hp");
        summonInfo[pos]["combat_status"] = summon:query("combat_status");
    end
end

-- 收集保存的状态
function collect_summon_data()
    local summonInfo = ME.user.dbase:query("summon");
    if sizeof(summonInfo) <= 0 then
        return {};
    end

    local ret = {};
    for _, pos in pairs(table.keys(summonInfo)) do
        local data = summonInfo[pos];

        ret[pos] = {
            class_id   = data["class_id"],
            action     = data["action"],
            cast_skill = data["cast_skill"],
            normal_pos = data["normal_pos"],
            instance_pos = data["instance_pos"],
            combat_status = data["combat_status"],
        };

        -- 把action、cast_skill清除掉
        data["action"] = nil;
        data["cast_skill"] = nil;
    end

    return ret;
end

function restore_summon_data(summonInfo)
    local info = ME.user.dbase:query("summon");
    if not info then
        return;
    end

    for _, pos in pairs(table.keys(info)) do
        local data = info[pos];
        local found = false;

        for p, d in pairs(summonInfo) do
            repeat
                if data["class_id"] ~= d["class_id"] then
                    break;
                end

                if not data["normal_pos"] and not data["instance_pos"] and pos == p or
                   data["normal_pos"] and data["normal_pos"] == d["normal_pos"] or
                   data["instance_pos"] and data["instance_pos"] == d["instance_pos"] then
                    -- 旧状态干掉
                    info[pos]["combat_status"] = nil;

                    info[pos] = table.add(data, d);

                    summonInfo[p] = nil;

                    found = true;

                    break;
                end
            until true;
        end

        if not found then
            info[pos]["action"] = nil;
            info[pos]["cast_skill"] = nil;
        end
    end

    ME.user.dbase:set("summon", info);
end

function applySummonProp(summonId, prop)
    local summonProp = ME.user.dbase:query("summon_prop", {});
    summonProp[summonId] = summonProp[summonId] or {};

    table.insert(summonProp[summonId], prop);

    ME.user.dbase:set("summon_prop", summonProp);

    -- 刷新下已经存在的召唤兽属性
    for _, summon in ipairs(ME.user.dbase:query("summon_list", {})) do
        if summonId == summon.classId then
            PropM.refreshEffect(summon, "summon_prop");
        end
    end
end

function fetchSummonProps(summon)
    local summonProp = ME.user.dbase:query("summon_prop", {});

    -- 记录在玩家身上的
    local props = summonProp[summon.classId] or {};

    -- 工坊升级带来的属性
    local blacksmithProps = SummonBlacksmithM.fetchBlacksmithProp(summon);

    return table.append(props, blacksmithProps);
end

-- 玩家复活(花费探索点复活)
whenHeroRevive = function()
    -- 玩家复活召唤
    for _, classId in ipairs(reviveCallSummon) do
        local info = query(classId);
        local mod = rules[info.rule];
        if mod and mod.canReviveCallSummon and mod.canReviveCallSummon(classId) then
            -- 玩家复活召唤
            doSummon(classId, {});
        elseif mod and mod.reviveCureSummon then
            -- 尝试执行一次恢复
            mod.reviveCureSummon(classId);
        end
    end
end

-- 建筑召唤
function buildingCallSummon(pos)
    local grid = DungeonM.getGridByPos(pos);

    if grid.type ~= GRID_TYPE_CALL_SUMMON or grid.state == GRID_STATE_DISABLE then
        trace("SummonM", "格子%d不是召唤兽建筑或者已经失效。", pos);
        return false;
    end

    -- 格子设置失效
    grid:changeState(GRID_STATE_DISABLE);

    local summonId = grid.summon_id;
    doSummon(summonId, {["pos"] = pos});

    -- 需要给玩家记录一下
    local summonBuilding = ME.user.dbase:query("summon_building") or {};
    summonBuilding[summonId] = tonumber(summonBuilding[summonId]) + 1;
    ME.user.dbase:set("summon_building", summonBuilding);

    return true;
end

-- 场上是否有未失效的召唤建筑
function hasCallSummonBuilding()
    local grids = DungeonM.getCurrentDungeon();
    for _, grid in pairs(grids) do
        if grid.type == GRID_TYPE_CALL_SUMMON
            and grid.state ~= GRID_STATE_DISABLE then
            return true;
        end
    end

    return false;
end